package guijavacommander;

import javax.swing.*;
import javax.swing.tree.TreeNode;
import javax.imageio.ImageIO;
import java.awt.*;
import java.awt.event.*;
import java.io.IOException;

/**
 * User: Deady
 * Date: 15.07.2009
 * Time: 11:13:07
 */
public class TreeList extends JList implements KeyListener, MouseListener, MouseMotionListener, Scrollable {

    protected int cellWidth = -1;
    private int currentElement;
    /**
     * Number of columns to create.
     */
    private int columnCount;
    /**
     * Number of rows per column. This is only used if the row height is
     * fixed.
     */
    private int rowsPerColumn;
    private TreeNode lastRightMouseSelected;

    public TreeList() {
        setLayoutOrientation(JList.VERTICAL_WRAP);
        setVisibleRowCount(0);
        for (MouseMotionListener l : getMouseMotionListeners()) {
            removeMouseMotionListener(l);            
        }
        for (MouseListener l : getMouseListeners()) {
            removeMouseListener(l);
        }
        for (KeyListener l : getKeyListeners()) {
            removeKeyListener(l);            
        }
        addMouseListener(this);
        addMouseMotionListener(this);
        addKeyListener(this);
        getSelectionModel().setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION);
        intallKeys();
        setCellRenderer(new CoolListCellRenderer());
        setFixedCellHeight(20);
        

    }


    public void keyTyped(KeyEvent e) {
        //To change body of implemented methods use File | Settings | File Templates.
    }

    public void keyPressed(KeyEvent e) {
        TreeListModel model = (TreeListModel) getModel();
        if (e.getKeyCode() == KeyEvent.VK_BACK_SPACE) {
            setCurrentElement( model.gotoParent());
            clearSelection();
            repaint();
            e.consume();
        } else if (e.getKeyCode() == KeyEvent.VK_ENTER) {
            goIntoElement(e);
        }
    }

    public void keyReleased(KeyEvent e) {
        //To change body of implemented methods use File | Settings | File Templates.
    }

    public void mouseClicked(MouseEvent e) {
        if (SwingUtilities.isLeftMouseButton(e) && e.getClickCount() == 2) {
            goIntoElement(e);
        }
    }

    private void goIntoElement(InputEvent e) {
        int index = getCurrentElement();
        TreeNode node = (TreeNode) getModel().getElementAt(index);
        if (!node.isLeaf()) {
            clearSelection();
            TreeListModel model = (TreeListModel) getModel();
            setCurrentElement(model.gotoChild(index));
            ensureIndexIsVisible(currentElement);
            repaint();
            e.consume();
        }
    }

    public void mousePressed(MouseEvent e) {
        requestFocus();
        if (SwingUtilities.isRightMouseButton(e)) {
            update();
            int index = convertLocationToModel(e.getX(), e.getY());
            lastRightMouseSelected = (TreeNode) getModel().getElementAt(index);
            setCurrentElement(index);
            selectCurrent();
            repaint();
            e.consume();
        }
        else if (SwingUtilities.isLeftMouseButton(e)) {
            update();
            int index = convertLocationToModel(e.getX(), e.getY());
            setCurrentElement(index);
            clearSelection();
            repaint();
            e.consume();
        }
    }

    public void mouseReleased(MouseEvent e) {
        //To change body of implemented methods use File | Settings | File Templates.
    }

    public void mouseEntered(MouseEvent e) {
        //To change body of implemented methods use File | Settings | File Templates.
    }

    public void mouseExited(MouseEvent e) {
        //To change body of implemented methods use File | Settings | File Templates.
    }


    public void mouseDragged(MouseEvent e) {
        if (SwingUtilities.isRightMouseButton(e)) {
            int index = convertLocationToModel(e.getX(), e.getY());
            if (index>=0 && index<getModel().getSize()) {
                TreeNode node = (TreeNode) getModel().getElementAt(index);
                if (node!=null && !node.equals(lastRightMouseSelected)) {
                    update();
                    setCurrentElement(index);
                    lastRightMouseSelected = node;
                    selectCurrent();
                    repaint();
                }
            }
            e.consume();
        }
        else if (SwingUtilities.isLeftMouseButton(e)) {
            update();
            int index = convertLocationToModel(e.getX(), e.getY());
            setCurrentElement(index);
            repaint();
            e.consume();
        }
    }

    public void mouseMoved(MouseEvent e) {
        //To change body of implemented methods use File | Settings | File Templates.
    }



    public void intallKeys() {
        getInputMap(WHEN_FOCUSED).put(KeyStroke.getKeyStroke(KeyEvent.VK_INSERT, 0), "selectCurrentRowAndMoveToNext");
        getInputMap(WHEN_FOCUSED).put(KeyStroke.getKeyStroke(KeyEvent.VK_DOWN, 0), "moveCurrentToNext");
        getInputMap(WHEN_FOCUSED).put(KeyStroke.getKeyStroke(KeyEvent.VK_UP, 0), "moveCurrentToPrevious");
        getInputMap(WHEN_FOCUSED).put(KeyStroke.getKeyStroke(KeyEvent.VK_LEFT, 0), "moveCurrentToPreviousColumn");
        getInputMap(WHEN_FOCUSED).put(KeyStroke.getKeyStroke(KeyEvent.VK_RIGHT, 0), "moveCurrentToNextColumn");
        getInputMap(WHEN_FOCUSED).put(KeyStroke.getKeyStroke(KeyEvent.VK_SPACE, 0), "selectCurrent");
        getInputMap(WHEN_FOCUSED).put(KeyStroke.getKeyStroke(KeyEvent.VK_HOME, 0), "moveCurrentToFirst");
        getInputMap(WHEN_FOCUSED).put(KeyStroke.getKeyStroke(KeyEvent.VK_END, 0), "moveCurrentToLast");

        getActionMap().put("selectCurrentRowAndMoveToNext", new AbstractAction() {
            public void actionPerformed(ActionEvent e) {
                selectCurrentRowAndMoveLeadToNext();
            }
        });
        getActionMap().put("moveCurrentToNext", new AbstractAction() {
            public void actionPerformed(ActionEvent e) {
                moveCurrentToNext();
            }
        });
        getActionMap().put("moveCurrentToPrevious", new AbstractAction() {
            public void actionPerformed(ActionEvent e) {
                moveCurrentToPrevious();
            }
        });
        getActionMap().put("moveCurrentToNextColumn", new AbstractAction() {
            public void actionPerformed(ActionEvent e) {
                moveCurrentToNextColumn();
            }
        });
        getActionMap().put("moveCurrentToPreviousColumn", new AbstractAction() {
            public void actionPerformed(ActionEvent e) {
                moveCurrentToPreviousColumn();
            }
        });
        getActionMap().put("selectCurrent", new AbstractAction() {
            public void actionPerformed(ActionEvent e) {
                selectCurrent();
            }
        });
        getActionMap().put("moveCurrentToFirst", new AbstractAction() {
            public void actionPerformed(ActionEvent e) {
                moveCurrentToFirst();
            }
        });
        getActionMap().put("moveCurrentToLast", new AbstractAction() {
            public void actionPerformed(ActionEvent e) {
                moveCurrentToLast();
            }
        });
    }


    private void moveCurrentToNext() {
        if (currentElement < getModel().getSize() - 1) {
            setCurrentElement(getCurrentElement()+1);
            ensureIndexIsVisible(currentElement);
            repaint();
        }
    }

    private void moveCurrentToPrevious() {
        if (currentElement > 0) {
            setCurrentElement(getCurrentElement()-1);
            ensureIndexIsVisible(currentElement);
            repaint();
        }
    }

    private void moveCurrentToNextColumn() {
        setCurrentElement(getNextColumnIndex(1));
        ensureIndexIsVisible(currentElement);
        repaint();
    }

    private void moveCurrentToPreviousColumn() {
        setCurrentElement(getNextColumnIndex(-1));
        ensureIndexIsVisible(currentElement);
        repaint();
    }

    public void moveCurrentToFirst() {
        setCurrentElement(0);
        ensureIndexIsVisible(currentElement);
        repaint();
    }

    public void moveCurrentToLast() {
        setCurrentElement(getModel().getSize()-1);
        ensureIndexIsVisible(currentElement);
        repaint();
    }

    private void selectCurrentRowAndMoveLeadToNext() {
        selectCurrent();
        moveCurrentToNext();
    }

    private int selectCurrent() {
        final int leadIndex = currentElement;
        final ListSelectionModel selectionModel = getSelectionModel();
        if (selectionModel.isSelectedIndex(leadIndex)) {
            selectionModel.removeSelectionInterval(leadIndex, leadIndex);
        } else {
            selectionModel.addSelectionInterval(leadIndex, leadIndex);
        }
        return leadIndex;
    }





    public int getCurrentElement() {
        return currentElement;
    }

    public void setCurrentElement(int currentElement) {
        int old = this.currentElement;
        this.currentElement = currentElement;
        firePropertyChange("currentElement", old, currentElement);
    }


    public void update() {
        int dataModelSize = getModel().getSize();
        Insets insets = getInsets();
        int listHeight = getHeight();
        int listWidth = getWidth();


        if (dataModelSize == 0) {
            rowsPerColumn = columnCount = 0;
            //preferredHeight = insets.top + insets.bottom;
            return;
        }


        int height;
        height = getFixedCellHeight();
        JPanel p = new JPanel();
        for(int index = 0; index < dataModelSize; index++) {
            Object value = getModel().getElementAt(index);
            Component c = getCellRenderer().getListCellRendererComponent(this, value, index, false, false);
            p.add(c);
            Dimension cellSize = c.getPreferredSize();
            if (getFixedCellWidth() == -1) {
                cellWidth = Math.max(cellSize.width, cellWidth);
            }
        }

        rowsPerColumn = Math.max(1, (listHeight - insets.top - insets.bottom) / height);
        columnCount = Math.max(1, dataModelSize / rowsPerColumn);
        if (dataModelSize > 0 && dataModelSize > rowsPerColumn && dataModelSize % rowsPerColumn != 0) {
            columnCount++;
        }

    }


    /**
     * Returns the column that the model index <code>index</code> will be
     * displayed in.
     */
    private int convertModelToColumn(int index) {
        int size = getModel().getSize();

        if ((index < 0) || (index >= size)) {
            return -1;
        }

        if (getLayoutOrientation() != JList.VERTICAL && rowsPerColumn > 0 &&
                columnCount > 1) {
            if (getLayoutOrientation() == JList.VERTICAL_WRAP) {
                return index / rowsPerColumn;
            }
            return index % columnCount;
        }
        return 0;
    }


    /**
     * Returns the row that the model index <code>index</code> will be
     * displayed in..
     */
    private int convertModelToRow(int index) {
        int size = getModel().getSize();

        if ((index < 0) || (index >= size)) {
            return -1;
        }

        if (getLayoutOrientation() != JList.VERTICAL && columnCount > 1 &&
                rowsPerColumn > 0) {
            if (getLayoutOrientation() == JList.VERTICAL_WRAP) {
                return index % rowsPerColumn;
            }
            return index / columnCount;
        }
        return index;
    }


    /**
     * Returns the model index for the specified display location.
     * If <code>column</code>x<code>row</code> is beyond the length of the
     * model, this will return the model size - 1.
     */
    private int getModelIndex(int column, int row) {
        switch (getLayoutOrientation()) {
            case JList.VERTICAL_WRAP:
                return Math.min(getModel().getSize() - 1, rowsPerColumn *
                        column + Math.min(row, rowsPerColumn - 1));
            case JList.HORIZONTAL_WRAP:
                return Math.min(getModel().getSize() - 1, row * columnCount +
                        column);
            default:
                return row;
        }
    }

    private int getNextColumnIndex(int amount) {
        update();
        if (getLayoutOrientation() != JList.VERTICAL) {
            int index = adjustIndex(currentElement);
            int size = getModel().getSize();

            if (index == -1) {
                return 0;
            } else if (size == 1) {
                // there's only one item so we should select it
                return 0;
            }

            int column = convertModelToColumn(index);
            int row = convertModelToRow(index);

            column += amount;
            if (column >= columnCount) {
                return getModel().getSize()-1;//last element
            }

            if (column < 0) {
                // 1st element
                return 0;
            }
            int maxRowCount = getRowCount(column);
            if (row >= maxRowCount) {
                row = maxRowCount-1;
            }
            return getModelIndex(column, row);
        }
        // Won't change the selection.
        return -1;
    }


    private int adjustIndex(int index) {
        return index < getModel().getSize() ? index : -1;
    }

    /**
     * Returns the number of rows in the given column.
     */
    private int getRowCount(int column) {
        if (column < 0 || column >= columnCount) {
            return -1;
        }
        if (getLayoutOrientation() == JList.VERTICAL ||
                  (column == 0 && columnCount == 1)) {
            return getModel().getSize();
        }
        if (column >= columnCount) {
            return -1;
        }
        if (getLayoutOrientation() == JList.VERTICAL_WRAP) {
            if (column < (columnCount - 1)) {
                return rowsPerColumn;
            }
            return getModel().getSize() - (columnCount - 1) * rowsPerColumn;
        }
        // JList.HORIZONTAL_WRAP
        int diff = columnCount - (columnCount * rowsPerColumn - getModel().getSize());

        if (column >= diff) {
            return Math.max(0, rowsPerColumn - 1);
        }
        return rowsPerColumn;
    }

    /**
     * Returns the row at location x/y.
     *
     * @param closest If true and the location doesn't exactly match a
     *                particular location, this will return the closest row.
     */
    private int convertLocationToRow(int x, int y0, boolean closest) {
        int size = getModel().getSize();

        if (size <= 0) {
            return -1;
        }
        Insets insets = getInsets();
            int row = (getFixedCellHeight() == 0) ? 0 :
                           ((y0 - insets.top) / getFixedCellHeight());
            if (closest) {
                if (row < 0) {
                    row = 0;
                }
                else if (row >= size) {
                    row = size - 1;
                }
            }
            return row;
    }


    /**
     * Returns the closest column to the passed in location.
     */
    private int convertLocationToColumn(int x, int y) {
        if (cellWidth > 0) {
            if (getLayoutOrientation() == JList.VERTICAL) {
                return 0;
            }
            Insets insets = getInsets();
            int col;
            col = (x - insets.left) / cellWidth;
            if (col < 0) {
                return 0;
            }
            else if (col >= columnCount) {
                return columnCount - 1;
            }
            return col;
        }
        return 0;
    }



    /**
     * Returns the closest row that starts at the specified y-location
     * in the passed in column.
     */
    private int convertLocationToRowInColumn(int y, int column) {
        int x = 0;

        if (getLayoutOrientation() != JList.VERTICAL) {
            x = column * cellWidth;
        }
        return convertLocationToRow(x, y, true);
    }

    /**
     * Returns the closest location to the model index of the passed in
     * location.
     */
    private int convertLocationToModel(int x, int y) {
        int row = convertLocationToRow(x, y, true);
        int column = convertLocationToColumn(x, y);

        if (row >= 0 && column >= 0) {
            return getModelIndex(column, row);
        }
        return -1;
    }





    public void clearSelection() {
        getSelectionModel().clearSelection();
    }


    class CoolListCellRenderer extends DefaultListCellRenderer {

        public Icon FILE_ICON;
        public Icon FOLDER_ICON;

        CoolListCellRenderer() {
            try {
                FOLDER_ICON = new ImageIcon(ImageIO.read(getClass().getClassLoader().getResourceAsStream("images/folder.png")));
                FILE_ICON = new ImageIcon(ImageIO.read(getClass().getClassLoader().getResourceAsStream("images/page_white.png")));
            } catch (IOException e) {
                e.printStackTrace();  //To change body of catch statement use File | Settings | File Templates.
            }
        }

        @Override
        public Component getListCellRendererComponent(JList list, Object value, int index, boolean isSelected, boolean cellHasFocus) {
            JLabel l = (JLabel) super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus);    //To change body of overridden methods use File | Settings | File Templates.
            if (index == currentElement && TreeList.this.hasFocus()) {
                l.setBorder(BorderFactory.createLineBorder(Color.BLACK, 1));
            } else {
                l.setBorder(BorderFactory.createEmptyBorder(1, 1, 1, 1));
            }
            TreeNode node = (TreeNode) value;
            l.setIcon(node.isLeaf()?FILE_ICON:FOLDER_ICON);
            return l;
        }
    }


/*
    public void ensureIndexIsVisible(int index) {
        Rectangle cellBounds = getCellBounds(index, index);
        if (cellBounds != null) {
            scrollRectToVisible(new Rectangle(cellBounds.x, cellBounds.y, 1, cellBounds.height));
        }

    }
*/
}
